OpenAI Realtime 세션 시작 실패 원인 분석
마지막 업데이트 2026-07-29
- 증상: 수사 일지 ③ — 용의자 심문 1: instruction 문제 가능성
- 원인: 수사 일지 ⑥ — 유력 용의자: 가장 가능성 높은 원인
- 수정·검증: 최종 판단
결론 — 유력 용의자부터 공개한다
The string did not match the expected pattern.는 OpenAI Realtime API가 직접 반환한 에러 메시지라기보다,
브라우저 WebRTC 엔진이 OpenAI Realtime 서버에서 받은 SDP answer 문자열을
RTCPeerConnection.setRemoteDescription()에서 파싱하다 실패한 런타임 에러일 가능성이 가장 높다.
현재 로그만으로는 instruction 문자열 문제라고 판단할 근거가 낮다. 로그에는 activity instruction 본문,
OpenAI API 400/500 응답, Call creation failed, Invalid SDP response 같은 서버/API 실패 흔적이 없다.
수사 일지 ① — 사건 기록: 전체 로그 흐름
AI 세션이 시작되지 않았다는 신고. 현장에 남은 것은 [AI_SESSION] Failed to start session과 정체불명의 한 줄 — The string did not match the expected pattern. 누가 이 문장을 남겼는지부터가 수수께끼였다. 사건 전후의 로그를 시간순으로 복원했다.
| 시간 | 레벨 | 로그 | 해석 |
|---|---|---|---|
| 08:58:57 - 09:02:25 | INFO | Entry request failed, continuing in standalone modeRoomNotFound |
게스트 방 입장 요청이 실패했지만, 코드상 standalone mode로 계속 진행한다. 직접적인 AI 세션 실패 원인은 아니다. |
| 09:08:04 - 09:19:42 | WARN | setSinkId failed: DOMException: A user gesture is required |
사용자 제스처 없이 오디오 출력 장치를 지정하려다 브라우저가 거부했다. catch 후 경고만 남기므로 세션 시작을 중단시키지는 않는다. |
| 09:20:36.242 | ERROR | [AI_SESSION] Failed to start sessionThe string did not match the expected pattern.activityId: activity-1779091460856-4 |
AI 세션 시작 중 실제 실패가 발생했다. activityId는 실패한 activity 식별자이며, 그 자체가 instruction 문제를 의미하지 않는다. |
| 09:20:36.243 | ERROR | [AI_SESSION] Failed to start session (handler) |
내부 startSession 에러가 handler에서 재로깅된 것이다. 원인은 직전 에러와 동일하다. |
앞선 두 이벤트(RoomNotFound, setSinkId)는 세션 시작을 중단시키지 않는 경로다. 사건은 09:20:36의 startSession 내부에서 일어났다. 범행 장소를 특정하려면 세션 시작 코드가 실제로 어떤 문자열을 다루는지 봐야 한다.
수사 일지 ② — 현장 검증: 관련 코드 경로
세션 시작 흐름은 다음과 같다.
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await createSession(
offer.sdp,
activity,
voice || avatar?.voice || DEFAULT_VOICE,
user,
avatar,
subAvatars,
realtimeModelVersion?.current,
signal,
turnDetectionRef.current.silence_duration_ms,
);
await pc.setRemoteDescription(answer);
answer.sdp는 우리 서버가 생성한 값이 아니라, 우리 서버가 OpenAI Realtime API
https://api.openai.com/v1/realtime/calls에 offer SDP를 전달한 뒤 text로 받은 OpenAI 서버의 SDP answer다.
formData.set("sdp", sdp);
formData.set("session", JSON.stringify(sessionConfig));
const res = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: formData,
});
const answerSdp = await res.text();
이 흐름에서 "패턴에 맞지 않는 문자열"을 만날 수 있는 지점은 셋 — instruction 문자열 처리, setSinkId 경고를 남긴 오디오 경로, 그리고 네트워크. 한 명씩 심문했다.
수사 일지 ③ — 용의자 심문 1: instruction 문제 가능성
낮음.
instruction 문제가 직접 원인이라면 보통 다음과 같은 흔적이 있어야 한다.
/api/realtime/call에서 400/500 응답Failed to create call또는Call creation failed- OpenAI API 응답 본문에 validation error
Invalid SDP response
제공된 로그에는 위 흔적이 없다. 또한 instruction 치환 코드는 정규식 기반 문자열 처리이며, 해당 메시지를 직접 생성하는 경로가 확인되지 않았다.
수사 일지 ④ — 용의자 심문 2: setSinkId 로그와의 관계
직접 관련은 낮음.
DOMException: A user gesture is required는 audioEl.setSinkId(audioOutputDeviceId)에서 발생한다.
이 호출은 try/catch로 감싸져 있고 실패해도 경고만 남긴 뒤 세션 시작 흐름은 계속 진행된다.
try {
await audioEl.setSinkId(audioOutputDeviceId);
} catch (e) {
console.warn("setSinkId failed:", e);
}
따라서 이 경고는 원하는 출력 장치로 소리가 나지 않을 수 있다는 신호이지,
The string did not match the expected pattern. 세션 실패의 직접 원인으로 보기는 어렵다.
수사 일지 ⑤ — 용의자 심문 3: 네트워크 문제 가능성
직접 가능성은 낮고, 간접 가능성은 제한적으로 있음.
일반적인 네트워크 문제라면 보통 다음 에러가 나온다.
Failed to fetchNetworkErrorAbortErrorSession creation timeout after 10sICE connection failed
현재 에러는 이미 받은 문자열을 브라우저가 특정 패턴으로 파싱하다 실패하는 형태다. 다만 프록시/서버 문제로 SDP 응답이 변형되거나 잘못 전달된다면 간접적으로 비슷한 현상이 날 수 있다.
세 용의자 모두 알리바이가 성립했다. 남는 것은 코드 경로의 마지막 줄 — 외부(OpenAI)에서 받은 SDP answer 문자열을 브라우저 WebRTC 파서에 넘기는 setRemoteDescription(). "패턴에 맞지 않는 문자열"이라는 문장과 정확히 맞아떨어지는 유일한 지점이다.
수사 일지 ⑥ — 유력 용의자: 가장 가능성 높은 원인
-
OpenAI Realtime SDP answer와 현재 브라우저 WebRTC 파서의 호환성 문제
특히 Safari/WebKit 계열이면 SDP 파싱이 더 엄격할 수 있다. -
SDP 문자열 형식 문제
줄바꿈, 지원하지 않는 SDP attribute, codec/header extension 등으로setRemoteDescription()에서 실패했을 수 있다. -
응답 객체 검증 부족
answer.type,answer.sdp, SDP 앞부분/길이를 실패 직전에 기록하지 않아 현재 로그만으로 확정이 어렵다.
유력 용의자는 특정됐지만 결정적 물증(실패 순간의 SDP 내용)이 없어 검거는 미완이다. 다음 범행 때 현장을 그대로 붙잡을 수 있도록 함정(로그 보강)과 방어선(검증·정규화·재시도)을 깐다.
검거 준비 — 권장 대처
1. 실패 지점 로그 보강
try {
await pc.setRemoteDescription(answer);
} catch (error) {
logger.error("Failed to apply realtime answer", {
errorName: error instanceof Error ? error.name : undefined,
errorMessage: error instanceof Error ? error.message : String(error),
errorStack: error instanceof Error ? error.stack : undefined,
answerType: answer?.type,
sdpStartsWithV: answer?.sdp?.startsWith("v="),
sdpLength: answer?.sdp?.length,
sdpHead: answer?.sdp?.slice(0, 300),
activityId: activity.id,
});
throw error;
}
2. setRemoteDescription 전 answer 검증
if (
answer?.type !== "answer" ||
typeof answer.sdp !== "string" ||
!answer.sdp.startsWith("v=")
) {
throw new Error("Invalid realtime answer SDP");
}
3. SDP 줄바꿈 정규화
const normalizedAnswer = {
type: "answer" as const,
sdp: answer.sdp.replace(/\r?\n/g, "\r\n"),
};
await pc.setRemoteDescription(normalizedAnswer);
4. 1회 재시도
setRemoteDescription 실패 시 기존 RTCPeerConnection을 닫고,
offer 생성부터 OpenAI Realtime call 생성까지 한 번만 재시도한다.
같은 SDP answer를 재사용하지 않는 것이 중요하다.
5. Safari/WebKit 대응
- Safari에서만 재현되는지 user agent와 함께 로깅한다.
- Safari에서 실패 시 Chrome/Edge 사용 안내 또는 fallback UX를 제공한다.
- 브라우저별 SDP 실패율을 모니터링한다.
추가 확인에 필요한 로그
error.name,error.stackanswer.typeanswer.sdp.startsWith("v=")answer.sdp.lengthanswer.sdp.slice(0, 300)- 브라우저 user agent
/api/realtime/call서버 로그의 OpenAI 응답 status와 SDP 길이
최종 판단
현재 로그 전체 흐름에서는 AI 세션 시작 중 WebRTC remote SDP 적용 실패가 가장 타당한 원인이다. instruction 문자열 문제, 일반 네트워크 문제, setSinkId 사용자 제스처 문제는 직접 원인으로 보기 어렵다.
다음 재현 시에는 SDP answer와 error metadata를 남기도록 로그를 보강해야 원인을 확정할 수 있다.